# This file is part of Neuroinfo Toolkit.
#
# Neuroinfo Toolkit is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Neuroinfo Toolkit is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Neuroinfo Toolkit.  If not, see <http://www.gnu.org/licenses/>.

import os
import stat
import errno
import tempfile
import shutil
import neuro.strings as strings
import neuro.config as config
from neuro.exceptions import IOException
from neuro.exceptions import IllegalArgumentException
from neuro.exceptions import FileNotFoundException
from neuro.exceptions import FileExistsException
from datetime import datetime
from pytz import timezone

def isExecutable(filename):
	'''
	Check if file is executable ::

		>>> isExecutable("/bin/bash")
		True

	:param filename: File name to test
	:type filename: str
	:rtype: bool
	:raises: :class:`~neuro.exceptions.FileNotFoundException`
	'''
	filename = canonical(filename)

	if(not exists(filename)):
		raise FileNotFoundException(filename)

	return os.access(canonical(filename), os.X_OK)

def isReadable(filename):
	'''
	Check if a file is readable ::
	
		>>> isReadable("/bin/bash")
		True
		
	:param filename: File name to test
	:type filename: str
	:rtype: bool
	:raises: :class:`~neuro.exceptions.FileNotFoundException`
	'''
	filename = canonical(filename)
	
	if(not exists(filename)):
		raise FileNotFoundException(filename)

	return os.access(canonical(filename), os.R_OK)

def isWritable(filename):
	'''
	Check if a file is writable ::

		>>> isWritable("/bin/bash")
		False
	
	:param filename: File name to test
	:type filename: str
	:rtype: bool
	:raises: :class:`~neuro.exceptions.FileNotFoundException`
	'''
	filename = canonical(filename)
	
	if(not exists(filename)):
		raise FileNotFoundException(filename)
	
	return os.access(filename, os.W_OK)

def isRegularFile(filename):
	'''
	Check that file is a regular file e.g. not a directory ::

		>>> isRegularFile("/bin/bash")
		True
		
	:param filename: File name to test
	:type filename: str
	:rtype: bool
	:raises: :class:`~neuro.exceptions.FileNotFoundException`
	'''
	filename = canonical(filename)

	if(not exists(filename)):
		raise FileNotFoundException(filename)

	return os.path.isfile(filename)

def exists(filename):
	'''
	Check if a file exists ::
	
		>>> exists("/bin/bash")
		True
		
	:param filename: File name to test
	:type filename: str
	:rtype: bool
	'''
	filename = canonical(filename)
	
	## --- permissions may cause os.path.exists to return False
	return os.access(filename, os.F_OK)
	
def fileGetContents(filename):
	'''
	Get file contents into a string ::
	
		>>> fileGetContents("/path/to/file.txt")
		'foobar\\nbizbat'
	
	:param filename: File to read
	:type filename: str
	:returns: File contents
	:rtype: str
	:raises: :class:`~neuro.exceptions.IOException`
	'''
	filename = canonical(filename)
	
	if(not isReadable(filename)):
		raise IOException(IOException.READ, filename)
	
	h = open(canonical(filename), 'r');
	
	string = h.read().strip()

	h.close()
	
	return string

def getExtension(filename, dot_depth=1):
	'''
	Retrieve file extension up to dot_depth ::
	
		>>> extension = getExtension("/path/to/file.txt")
	
	:param filename: File name to process
	:type filename: str
	:param dot_depth: Accept this many dots (not implemented)
	:type dot_depth: int
	:returns: File extension
	:rtype: str
	''' 
	filename = canonical(filename)
	
	list = filename.split('.')[-1]
	
	if(list == filename or list == ""):
		return ""
	else:
		return "." + list

def basename(filename):
	'''
	Retrieve the basename of a file i.e., file name without directory ::
	
		>>> basename("/path/to/file.txt")
		'file.txt'
		
	:param filename: File name
	:type filename: str
	:returns: File name without directory
	:rtype: str
	'''
	filename = canonical(filename)
	
	base = os.path.basename(filename)
	
	return base

def dirname(filename):
	'''
	Retrieve the parent directory of a file i.e., directory without file name ::
	
		>>> dirname("/path/to/file.txt")
		'/path/to'
		
	:param filename: File name
	:type filename: str
	:returns: Directory without file name
	:rtype: str
	'''
	filename = canonical(filename)
	
	base = os.path.dirname(filename)
	
	if(base == ""):
		return os.getcwd()
	else:
		return base


def dirList(dir, recursive=False, filter='.*'):
	'''
	Perform a full directory listing, like glob ::
	
		files = dirList("/path/to/dir")
		files = dirList("/path/to/dir", True)
		files = dirList("/path/to/dir", True, "\.dcm")
		files = dirList("/path/to/dir", False, Dicom())
	
	:param dir: Directory to search
	:type dir: str
	:param recursive: Traverse directory tree
	:type recursive: bool
	:param filter: Filter files returned based on regular expression or :class:`~neuro.models.resource.Resource`
	:type filter: regex, :class:`~neuro.models.resource.Resource`
	:returns: List of files
	:rtype: list
	:raises: :class:`~neuro.exceptions.FileNotFoundException`, 
			 :class:`~neuro.exceptions.IOException`
	'''
	dir = canonical(dir)

	if(not isinstance(recursive, bool)):
		raise IllegalArgumentException("Recursive switch must be boolean")

	## --- validate filter parameter
	if(isinstance(filter, basestring)):
		filterType = "string"
		filter = filter.strip()
		if(filter == ""):
			raise IllegalArgumentException("If filter is defined as a string, it cannot be empty")
	elif(getattr(filter, "checkFormat", False)):
		filterType = "resource"
	else:
		raise IllegalArgumentException("Filter must be an instance of str or Resource (i.e. should have checkFormat() method)")
	
	if(not isReadable(dir)):
		raise IOException(IOException.READ, dir)
	
	list = []

	if(recursive):
		for root, dirs, files in os.walk(dir):
			for name in files:
				file = os.path.join(root, name)
				
				if(filterType == "resource"):
					if(filter.checkFormat(file)):
						list.append(file)
				else:
					match = strings.regex(filter, file)
					if(match):
							list.append(file)
	else:
		for name in os.listdir(dir):
			file = os.path.join(dir, name)
			
			if(filterType == "resource"):
				if(filter.checkFormat(file)):
					list.append(file)
			else:
				match = strings.regex(filter, file)
				if(match):
					list.append(file)

	return list

def mkdir(dir, perms=0700):
	'''
	Recursively make directory ::
	
		>>> mkdir("/path/to/dir")
		'/path/to/dir'
	
	:param dir: Directory to create
	:type dir: str
	:param perms: Permissions to apply to directory
	:type perms: octal permissions
	:returns: Directory that was created
	:rtype: str
	:raises: :class:`~neuro.exceptions.IOException`
	'''
	dir = canonical(dir)
	
	try:
		os.makedirs(dir, perms)
	except OSError, e:
		if(e.errno == errno.EEXIST):
			raise FileExistsException(dir)
		elif(e.errno == errno.EACCES):
			raise IOException(IOException.WRITE, dirname(dir))
	
	return dir
	
def poke(filename, touch=False):
	'''
	Test that you can create a file, creating any directories as necessary ::
	
		>>> poke("/path/to/file.txt")
		True
		
	:param filename: File to touch
	:type filename: str
	:param touch: Actually create a file
	:type touch: bool
	:raises: :class:`~neuro.exceptions.IOException`
	'''
	filename = canonical(filename)

	if(not isinstance(touch, bool)):
		raise IllegalArgumentException("Touch must be an instance of bool")
	
	if(exists(filename)):
		raise FileExistsException(filename)
	
	## --- get directory name
	dir = dirname(filename)
	
	## --- check if it exists or writable
	if(not exists(dir)):
		mkdir(dir)
	elif(not isWritable(dir)):
		raise IOException(IOException.WRITE, dir)
	
	if(touch):
		h = open(filename, "wb")
		h.close()
	
def filePutContents(filename, content, overwrite=True):
	'''
	Put contents of string into file ::
	
		>>> string = "write me to a file"
		>>> filePutContents("/path/to/file.txt", string)
		
	:param filename: File name to put contents
	:type filename: str
	:param content: Content to write to file
	:type content: str
	:raises: :class:`~neuro.exceptions.FileExistsException`
	'''
	filename = canonical(filename)

	if(not isinstance(content, basestring)):
		raise IllegalArgumentException("Content must be an instance of str")
	
	content = content.strip()

	if(content == ""):
		raise IllegalArgumentException("Content cannot be empty")
	
	## --- tests that you can write to this file
	try:
		poke(filename)
	except FileExistsException, e:
		if(not overwrite):
			raise e

	## --- open file handle in write mode, write content, then close
	h = open(filename, 'w')
	h.write(content)
	h.close()

def fileAddContents(filename, content):
	'''
	Append content to a file ::

		>>> string = "write me to the end of a file"
		>>> fileAddContents("/path/to/file.txt", string)

	:param filename: File name to add contents
	:type filename: str
	:param content: Content to add to file
	:type content: str
	:raises: :class:`~neuro.exceptions.IOException`
	'''
	if(not isinstance(content, basestring)):
		raise IllegalArgumentException("Content must be an instance of str")

	filename = canonical(filename)
	content = content.strip()
	
	if(content == ""):
		raise IllegalArgumentException("Content cannot be empty")

	## --- tests that you can write to this file
	if(not exists(filename)):
		poke(filename)
	elif(not isWritable(filename)):
		raise IOException(IOException.WRITE, filename)

	## --- open file handle in append mode and write content
	h = open(canonical(filename), 'a')
	h.write(content)
	h.close()

def canonical(filename):
	'''
	Canonicalize a filename i.e., expand links and other special characters ::
	
		>>> canonical("~/file.txt")
		'/home/jdoe/file.txt'
		
	:param filename: File name to canonicalize
	:type filename: str
	:returns: Expanded file name
	:rtype: str
	'''
	if(not isinstance(filename, basestring)):
		raise IllegalArgumentException("Filename must be an instance of str")

	filename = filename.strip()

	if(filename == ""):
		raise IllegalArgumentException("Filename cannot be empty")
	
	filename = os.path.expanduser(filename)

	return os.path.abspath(filename)

def makeTempDir(root="./"):
	'''
	Create a temporary directory ::
	
		>>> makeTempDir("/tmp")
		'/tmp/tL5nBC'
		
	:param root: Root location of temporary directory
	:type root: str
	:returns: Name of temporary directory
	:rtype: str
	:raises: :class:`~neuro.exceptions.IOException`
	'''
	root = canonical(root)
	
	if(not exists(root)):
		mkdir(root)
	elif(not isWritable(root)):
		raise IOException(IOException.WRITE, dirname(root))
	
	tmpDir = tempfile.mkdtemp("", "", root)
	
	return tmpDir

def isDir(filename):
	'''
	Check if a file is a directory ::
	
		>>> isDir("/path/to/dir")
		True
		
	:param filename: Directory to test
	:type filename: str
   	:rtype: bool
	:raises: :class:`~neuro.exceptions.FileNotFoundException`
	''' 
	filename = canonical(filename)
	
	if(not exists(filename)):
		raise FileNotFoundException(filename)
	
	return os.path.isdir(filename)

def delete(filename, recursive=False):
	'''
	.. warning:: Use with extreme caution, especially with recursive=True
	
	Delete a file or directory ::
	
		>>> delete("/path/to/file.txt")
		
	:param filename: File or directory to delete
	:type filename: str
	:param recursive: Delete directory and all sub-contents
	:type recursive: bool
	:raises: :class:`~neuro.exceptions.FileNotFoundException`, 
			 :class:`~neuro.exceptions.IOException`
	'''
	filename = canonical(filename)
	
	if(not isWritable(filename)):
		raise IOException(IOException.WRITE, filename)
	
	if(isDir(filename)):
		if(os.listdir(filename) == []):
			os.rmdir(filename)
		else:
			if(recursive):
				shutil.rmtree(filename)
			else:
				raise IOException(IOException.DEL_FULL, filename)
	else:
		os.remove(filename)
	
def getFilePermissions(filename):
	'''
	Get file octal permissions ::

		>>> getFilePermissions("/path/to/file")
		'0755'
		
	:param filename: File name
	:type filename: str
	:rtype: str
	:raises: :class:`~neuro.exceptions.IOException`
	'''
	filename = canonical(filename)
	
	if(not exists(filename)):
		raise FileNotFoundException(filename)
	
	mode = stat.S_IMODE(os.stat(filename).st_mode)

	return oct(mode)

def percentFull(filename):
	'''
	Retrieve the percent capacity of a partition given a mount point or 
	filename ::
	
		>>> percentFull("/path/to/some/file.txt")
		47.63481161525493
		
	:param filename: Any file on any partition
	:type filename: str
	:returns: Percent capacity of partition
	:rtype: float
	:raises: :class:`~neuro.exceptions.FileNotFoundException`
	'''
	filename = canonical(filename)
	
	if(not exists(filename)):
		raise FileNotFoundException(filename)

	(total, free) = getFilesystemInfo(filename)

	return 100 * (total - free) / float(total)

def getFilesystemInfo(filename):
	'''
	Get filesystem capacity (in bytes) given a mount point or filename. Results 
	are for a non-superuser ::

		>>> getFilesystemInfo("/path/to/some/file.txt")
		(490902953984, 257062084608)
		
	:param filename: Any file on any partition
	:type filename: str
	:returns: (total_space, free_space)
	:rtype: tuple
	'''
	filename = canonical(filename)

	if(not exists(filename)):
		raise FileNotFoundException(filename)
	
	result = os.statvfs(filename)

	total = result.f_blocks * result.f_frsize
	free = result.f_bavail * result.f_frsize

	return (total, free)

def copy(old, new):
	'''
	Copy a file to a different location ::

		>>> copy("/path/to/old.txt", "/path/to/new.txt")
		
	:param old: File location
	:type old: str
	:param new: Desired file location
	:type new: str
	:raises: :class:`~neuro.exceptions.FileNotFoundException`, 
			 :class:`~neuro.exceptions.FileExistsException`
	'''
	if(not isinstance(old, basestring)):
		raise IllegalArgumentException("Old Filename must be an instance of str")
	elif(not isinstance(new, basestring)):
		raise IllegalArgumentException("New filename must be an instance of str")

	old = old.strip()
	new = new.strip()

	if(old == ""):
		raise IllegalArgumentException("Old filename cannot be empty")
	elif(new == ""):
		raise IllegalArgumentException("New filename cannot be empty")

	if(not exists(old)):
		raise FileNotFoundException(old)
	elif(exists(new)):
		raise FileExistsException(new)

	shutil.copy2(canonical(old), canonical(new))
	
def backup(file, older_than=None, new_dir=None, suffix=None):
	'''
	Backup a file or directory
	
	:param file: File or directory
	:type file: str
	:param older_than: Backup only if older than (in seconds)
	:type older_than: int
	:param new_dir: New parent directory
	:type new_dir: str
	:param suffix: Default ISO-8601 with offset according to neuro.config.timezone
	:type suffix: str
	:return: Backed up directory name, or None
	:rtype: str|None
	'''
	file = os.path.realpath(os.path.expanduser(file))
	
	dir_name = os.path.dirname(file)
	base_name = os.path.basename(file) 
	
	if not new_dir:
		new_dir = os.path.dirname(file)
	
	## --- backup if (modification_time > older_than) seconds from now
	if older_than != None:
		mtime = datetime.utcfromtimestamp(os.path.getmtime(file))
		if (datetime.utcnow() - mtime).seconds < older_than:
			return None
	
	## --- default append timestamp suffix
	if suffix == None:
		tz = timezone(config.timezone)
		suffix = "_" + datetime.now(tz).isoformat()
	
	## --- rename file
	new = os.path.join(new_dir, base_name + suffix)
	os.rename(file, new)
	
	return new